/*
 * Copyright 2017-present, Yudong (Dom) Wang
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.wisdom.tool.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.*;
import net.htmlparser.jericho.Source;
import net.htmlparser.jericho.SourceFormatter;
import org.apache.commons.codec.Charsets;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dom4j.*;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.wisdom.tool.cache.RESTCache;
import org.wisdom.tool.constant.RESTConst;
import org.wisdom.tool.model.*;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.*;
import java.util.Map.Entry;

/**
 * @ClassName: RESTUtil
 * @Description: Rest utility
 * @Author: Yudong (Dom) Wang
 * @Email: wisdomtool@qq.com
 * @Date: 2017-07-22 PM 10:42:57
 * @Version: Wisdom RESTClient V1.2
 */
public class RESTUtil {
    private static Logger log = LogManager.getLogger(RESTUtil.class);

    /**
     * @param @param  jf
     * @param @param  clas
     * @param @return
     * @return T
     * @throws
     * @Title: toOject
     * @Description: Json file to object
     */
    public static <T> T toOject(File jf, Class<T> clas) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return (T) mapper.readValue(jf, clas);
        } catch (Exception e) {
            log.error("Change json file [" + jf.getName() + "] to object failed.", e);
        }

        return null;
    }

    /**
     * @param @param  is
     * @param @param  clas
     * @param @return
     * @return T
     * @throws
     * @Title: toOject
     * @Description: Json file input stream to object
     */
    public static <T> T toOject(InputStream is, Class<T> clas) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return (T) mapper.readValue(is, clas);
        } catch (Exception e) {
            log.error("Change json file input stream to object failed.", e);
        }

        return null;
    }

    /**
     * @param @param  content
     * @param @param  clas
     * @param @return
     * @return T
     * @throws
     * @Title: toOject
     * @Description: Json content to object
     */
    public static <T> T toOject(String content, Class<T> clas) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return (T) mapper.readValue(content, clas);
        } catch (Exception e) {
            log.error("Change json text [" + content + "] to object failed.", e);
        }

        return null;
    }

    /**
     * @param @param  instance
     * @param @return
     * @return String
     * @throws
     * @Title: tojsonText
     * @Description: Instance to json string
     */
    public static <T> String tojsonText(T instance) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(instance);
        } catch (Exception e) {
            log.error("Write object [" + instance + "] as json string failed.", e);
        }

        return StringUtils.EMPTY;
    }

    /**
     * @param @param  jf
     * @param @param  instance
     * @param @return
     * @return void
     * @throws
     * @Title: toJsonFile
     * @Description: Instance to json file
     */
    public static <T> void toJsonFile(File jf, T instance) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            mapper.writerWithDefaultPrettyPrinter().writeValue(jf, instance);
        } catch (Exception e) {
            log.error("Write object [" + instance + "] as json file [" + jf.getName() + "] failed.", e);
        }
    }

    /**
     * @param @return
     * @return Comparator<String>
     * @throws
     * @Title: getComparator
     * @Description: New a comparator
     */
    private static Comparator<String> getComparator() {
        Comparator<String> c = new Comparator<String>() {
            public int compare(String o1, String o2) {
                return o1.compareTo(o2);
            }
        };

        return c;
    }

    /**
     * @param @param e
     * @return void
     * @throws
     * @Title: sort
     * @Description: Sort JSON element
     */
    public static void sort(JsonElement e) {
        if (e.isJsonNull()) {
            return;
        }

        if (e.isJsonPrimitive()) {
            return;
        }

        if (e.isJsonArray()) {
            JsonArray a = e.getAsJsonArray();
            for (Iterator<JsonElement> it = a.iterator(); it.hasNext(); ) {
                sort(it.next());
            }
            return;
        }

        if (e.isJsonObject()) {
            Map<String, JsonElement> tm = new TreeMap<String, JsonElement>(getComparator());
            for (Entry<String, JsonElement> en : e.getAsJsonObject().entrySet()) {
                tm.put(en.getKey(), en.getValue());
            }

            for (Entry<String, JsonElement> en : tm.entrySet()) {
                e.getAsJsonObject().remove(en.getKey());
                e.getAsJsonObject().add(en.getKey(), en.getValue());
                sort(en.getValue());
            }
            return;
        }
    }

    /**
     * @param @param  jsonStr
     * @param @return
     * @return String
     * @throws
     * @Title: sort
     * @Description: Sort JSON string by key
     */
    public static String sort(String jsonStr) {
        Gson g = new GsonBuilder().setPrettyPrinting().create();
        JsonParser p = new JsonParser();
        JsonElement e = p.parse(jsonStr);

        sort(e);

        return g.toJson(e);
    }

    /**
     * @param @param  jtxt1
     * @param @param  jtxt2
     * @param @return
     * @return boolean
     * @throws
     * @Title: compare
     * @Description: Compare two json text
     */
    public static boolean compare(String jstr1, String jstr2) {
        JsonParser p = new JsonParser();
        JsonElement e1 = p.parse(jstr1);
        JsonElement e2 = p.parse(jstr2);

        sort(e1);
        sort(e2);

        return e1.equals(e2);
    }

    /**
     * @param @param  content
     * @param @return
     * @return boolean
     * @throws
     * @Title: isJson
     * @Description: Check if it is JSON string
     */
    public static boolean isJson(String json) {
        if (StringUtils.isEmpty(json)) {
            return false;
        }

        if (!StringUtils.contains(json, "{")) {
            return false;
        }

        JsonParser p = new JsonParser();
        try {
            p.parse(json);
        } catch (JsonSyntaxException e) {
            log.debug("Bad json format: " + lines(1) + json);
            return false;
        }

        return true;
    }

    /**
     * @param @param  xml
     * @param @return
     * @return boolean
     * @throws
     * @Title: isXml
     * @Description: Check if it is XML string
     */
    public static boolean isXml(String xml) {
        if (StringUtils.isEmpty(xml)) {
            return false;
        }

        try {
            DocumentHelper.parseText(xml);
        } catch (DocumentException e) {
            log.debug("Bad xml format: " + lines(1) + xml);
            return false;
        }

        return true;
    }

    /**
     * @param @param  html
     * @param @return
     * @return boolean
     * @throws
     * @Title: isHtml
     * @Description: Check if it is HTML string
     */
    public static boolean isHtml(String html) {
        if (StringUtils.isEmpty(html)) {
            return false;
        }

        if (StringUtils.containsIgnoreCase(html, RESTConst.HTML_LABEL)) {
            return true;
        }

        return false;
    }

    /**
     * @param @param  json
     * @param @return
     * @return String
     * @throws
     * @Title: prettyJson
     * @Description: Pretty JSON formatter
     */
    public static String prettyJson(String json) {
        if (StringUtils.isBlank(json)) {
            return StringUtils.EMPTY;
        }

        JsonParser jp = new JsonParser();
        JsonElement je = jp.parse(json);
        Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create();
        String prettyJson = gson.toJson(je);
        return prettyJson;
    }

    /**
     * @param @param  xml
     * @param @return
     * @return String
     * @throws
     * @Title: prettyXml
     * @Description: Pretty XML formatter
     */
    public static String prettyXml(String xml) {
        XMLWriter xw = null;

        if (StringUtils.isBlank(xml)) {
            return StringUtils.EMPTY;
        }

        try {
            Document doc = DocumentHelper.parseText(xml);

            // Format
            OutputFormat format = OutputFormat.createPrettyPrint();
            format.setEncoding(Charsets.UTF_8.name());

            // Writer
            StringWriter sw = new StringWriter();
            xw = new XMLWriter(sw, format);
            xw.write(doc);
            return sw.toString();
        } catch (Exception e) {
            log.error("Failed to format xml.", e);
        } finally {
            close(xw);
        }

        return StringUtils.EMPTY;
    }

    /**
     * @param @param  html
     * @param @return
     * @return String
     * @throws
     * @Title: prettyHtml
     * @Description: Pretty HTML formatter
     */
    public static String prettyHtml(String html) {
        if (StringUtils.isBlank(html)) {
            return StringUtils.EMPTY;
        }

        try {
            // Writer
            StringWriter sw = new StringWriter();
            new SourceFormatter(new Source(html))
                    .setIndentString("    ")
                    .setTidyTags(true)
                    .setCollapseWhiteSpace(true)
                    .writeTo(sw);
            return sw.toString();
        } catch (Exception e) {
            log.error("Failed to format html.", e);
        }

        return StringUtils.EMPTY;
    }

    /**
     * @param @param  txt
     * @param @return
     * @return String
     * @throws
     * @Title: format
     * @Description: Format text
     */
    public static String format(String txt) {
        if (StringUtils.isBlank(txt)) {
            return StringUtils.EMPTY;
        }

        if (isJson(txt)) {
            return prettyJson(txt);
        }

        if (isHtml(txt)) {
            return prettyHtml(txt);
        }

        if (isXml(txt)) {
            return prettyXml(txt);
        }

        return txt;
    }

    /**
     * @param @param millis
     * @return void
     * @throws
     * @Title: sleep
     * @Description: Thread sleep
     */
    public static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            log.error("Sleep interrupted.", e);
        }
    }

    /**
     * @param @param  num
     * @param @return
     * @return String
     * @throws
     * @Title: lines
     * @Description: Get lines
     */
    public static String lines(int num) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < num; i++) {
            sb.append("\r\n");
        }
        return sb.toString();
    }

    /**
     * @param @return
     * @return String
     * @throws
     * @Title: nowDate
     * @Description: Get now date
     */
    public static String nowDate() {
        SimpleDateFormat fmat = new SimpleDateFormat(RESTConst.DATE_FORMAT);
        return fmat.format(new Date());
    }

    /**
     * @param @param  path
     * @param @return
     * @return InputStream
     * @throws
     * @Title: getInputStream
     * @Description: get input stream
     */
    public static InputStream getInputStream(String path) {
        return RESTUtil.class.getClassLoader().getResourceAsStream(path);
    }

    /**
     * @param @param is
     * @return void
     * @throws
     * @Title: close
     * @Description: Close input stream
     */
    public static void close(InputStream is) {
        if (null == is) {
            return;
        }
        try {
            is.close();
            is = null;
        } catch (IOException e) {
            log.error("Failed to close input stream", e);
        }
    }

    /**
     * @param @param  code
     * @param @param  args
     * @param @return
     * @return Cause
     * @throws
     * @Title: getCause
     * @Description: Get test failure/error cause
     */
    public static Cause getCause(ErrCode code, String... args) {
        Cause c = new Cause();
        if (null == code) {
            return c;
        }

        Causes cs = RESTCache.getCauses();
        if (null == cs || MapUtils.isEmpty(cs.getCauses())) {
            return c;
        }

        c = new Cause(cs.getCauses().get(code.getCode()));
        c.setCode(code);
        if (null == args) {
            return c;
        }

        String msgStr = c.getMsgEnUS();
        for (int i = 0; i < args.length; i++) {
            msgStr = msgStr.replaceFirst("<" + (i + 1) + ">", "[ " + args[i] + " ]");
        }
        c.setMsgEnUS(msgStr);

        msgStr = c.getMsgZhCN();
        for (int i = 0; i < args.length; i++) {
            msgStr = msgStr.replaceFirst("<" + (i + 1) + ">", "[ " + args[i] + " ]");
        }
        c.setMsgZhCN(msgStr);
        return c;
    }

    /**
     * @param @param hist
     * @param @param oldRsp
     * @param @param newRsp
     * @return void
     * @throws
     * @Title: result
     * @Description: Set test result
     */
    public static void result(HttpHists hists, HttpHist hist, HttpRsp newRsp) {
        Cause cs = null;

        HttpRsp oldRsp = hist.getRsp();
        String oldBdy = oldRsp.getBody();
        String newBdy = newRsp.getBody();

        if (null == oldBdy) {
            oldBdy = StringUtils.EMPTY;
        }

        if (null == newBdy) {
            newBdy = StringUtils.EMPTY;
        }

        Integer oldSC = oldRsp.getStatusCode();
        Integer newSC = newRsp.getStatusCode();

        hist.setResult(Results.PASS);
        hist.getRsp().setDate(newRsp.getDate());
        hist.getRsp().setTime(newRsp.getTime());
        hist.getRsp().setRawTxt("");
        hist.getRsp().setBody("");
        hist.getRsp().setHeaders(null);
        hist.getReq().setRawTxt("");
        hist.getReq().setBody("");
        hist.getReq().setHeaders(null);
        hist.getReq().setCookies(null);

        if (null == oldSC || null == newSC) {
            hist.setResult(Results.ERROR);
            cs = RESTUtil.getCause(ErrCode.HTTP_REQUEST_FAILED);
            hist.setCause(cs.toString());
            hists.countErr();
            return;
        }

        if (!oldSC.equals(newSC)) {
            hist.setResult(Results.FAILURE);
            cs = RESTUtil.getCause(ErrCode.INCONSISTENT_STATUS, String.valueOf(newSC), String.valueOf(oldSC));
            hist.setCause(cs.toString());
            hists.countFail();
            return;
        }

        if (!hist.getAssertBdy()) {
            cs = RESTUtil.getCause(ErrCode.SUCCESS);
            hist.setCause(cs.toString());
            hists.countPass();
            return;
        }

        boolean isFailed = false;
        if (isJson(oldBdy)) {
            if (!diff(oldBdy, newBdy, hist.getExcludedNodes())) {
                isFailed = true;
            }
        } else {
            if (!oldBdy.equals(newBdy)) {
                isFailed = true;
            }
        }

        if (isFailed) {
            hist.setResult(Results.FAILURE);
            cs = RESTUtil.getCause(ErrCode.INCONSISTENT_BODY);
            hist.setCause(cs.toString());
            hists.countFail();
            return;
        }

        cs = RESTUtil.getCause(ErrCode.SUCCESS);
        hist.setCause(cs.toString());
        hists.countPass();
    }

    /**
     * @param @param  path
     * @param @return
     * @return String
     * @throws
     * @Title: replacePath
     * @Description: replace file path
     */
    public static String replacePath(String path) {
        return StringUtils.replaceOnce(path,
                RESTConst.WISDOM_TOOL,
                RESTConst.WORK + File.separatorChar);
    }

    /**
     * @param @return
     * @return String
     * @throws
     * @Title: getReportPath
     * @Description: get report path
     */
    public static String getPath(String subPath) {
        StringBuilder sb = new StringBuilder();
        sb.append(RESTConst.WORK).append(File.separatorChar);

        if (StringUtils.isNotEmpty(subPath)) {
            sb.append(subPath).append(File.separatorChar);
        }

        return sb.toString();
    }

    /**
     * @param @param w
     * @return void
     * @throws
     * @Title: close
     * @Description: Close writer
     */
    public static void close(XMLWriter xw) {
        if (null == xw) {
            return;
        }
        try {
            xw.close();
            xw = null;
        } catch (IOException e) {
            log.error("Failed to close writer.", e);
        }
    }

    /**
     * @param @param  num
     * @param @return
     * @return String
     * @throws
     * @Title: dup
     * @Description: Get duplicate string by specified number and chars
     */
    public static String dup(int num, String chars) {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < num; i++) {
            sb.append(chars);
        }

        return sb.toString();
    }

    /**
     * @param @param e
     * @param @param layer
     * @param @param sb
     * @return layer
     * @throws
     * @Title: jsonTree
     * @Description: To generate a JSON tree
     */
    public static void jsonTree(JsonElement e, int layer, StringBuilder sb) {
        if (e.isJsonNull()) {
            return;
        }

        if (e.isJsonPrimitive()) {
            return;
        }

        if (e.isJsonArray()) {
            JsonArray ja = e.getAsJsonArray();
            if (ja.size() > 0) {
                jsonTree(ja.get(0), layer, sb);
            }
            return;
        }

        if (e.isJsonObject()) {
            String line = RESTConst.LINE;
            String type = RESTConst.UNKNOWN;
            String spaces = "    ";
            String vertLine = "│   ";

            String indent = dup(layer, spaces);

            layer++;
            if (layer <= 0) {
                line = "   ";
            }

            Set<Entry<String, JsonElement>> es = e.getAsJsonObject().entrySet();
            for (Entry<String, JsonElement> en : es) {
                indent = dup(layer, spaces);
                if (layer >= 2) {
                    indent = dup(1, spaces) + dup(layer - 1, vertLine);
                }

                sb.append(indent).append(line).append(en.getKey()).append(" [");

                if (en.getValue().isJsonArray()) {
                    type = Array.class.getSimpleName();
                } else if (en.getValue().isJsonObject()) {
                    type = Object.class.getSimpleName();
                } else if (en.getValue().isJsonPrimitive()) {
                    JsonPrimitive jp = en.getValue().getAsJsonPrimitive();
                    if (jp.isBoolean()) {
                        type = Boolean.class.getSimpleName();
                    } else if (jp.isNumber()) {
                        type = Number.class.getSimpleName();
                    } else if (jp.isString()) {
                        type = String.class.getSimpleName();
                    }
                } else if (en.getValue().isJsonNull()) {
                    type = null + "";
                }

                sb.append(type.toLowerCase()).append("]").append(lines(1));
                jsonTree(en.getValue(), layer, sb);
            }
        }
    }

    /**
     * @param @param e
     * @param @param layer
     * @param @param sb
     * @return void
     * @throws
     * @Title: xmlTree
     * @Description: To generate a XML tree
     */
    public static void xmlTree(Element e, int layer, StringBuilder sb) {
        if (e.nodeCount() <= 0) {
            return;
        }

        String spaces = "    ";
        String vertLine = "│   ";
        String line = RESTConst.LINE;
        String type = RESTConst.UNKNOWN;
        String indent = dup(layer, spaces);

        layer++;
        if (layer <= 0) {
            line = "   ";
        }

        @SuppressWarnings("unchecked")
        List<Element> es = e.elements();
        for (Element ce : es) {
            indent = dup(layer, spaces);
            if (layer >= 2) {
                indent = dup(1, spaces) + dup(layer - 1, vertLine);
            }

            if (!ce.elements().isEmpty() || ce.attributeCount() > 0) {
                type = Object.class.getSimpleName();
            } else if (StringUtils.isNotEmpty(ce.getText()) && StringUtils.isNumeric(ce.getStringValue())) {
                type = Number.class.getSimpleName();
            } else {
                type = String.class.getSimpleName();
            }

            /* Element */
            sb.append(indent).append(line)
                    .append(ce.getName()).append(" [")
                    .append(type.toLowerCase())
                    .append("]").append(lines(1));

            /* Attributes */
            if (ce.attributeCount() > 0) {
                indent = dup(layer + 1, spaces);
                if (layer + 1 >= 2) {
                    indent = dup(1, spaces) + dup(layer, vertLine);
                }

                @SuppressWarnings("unchecked")
                List<Attribute> as = ce.attributes();
                for (Attribute a : as) {
                    if (StringUtils.isNotEmpty(ce.getText()) && StringUtils.isNumeric(a.getValue())) {
                        type = Number.class.getSimpleName();
                    } else {
                        type = String.class.getSimpleName();
                    }

                    sb.append(indent).append(RESTConst.LINE)
                            .append(a.getName()).append(" [")
                            .append(type.toLowerCase())
                            .append("]").append(lines(1));
                }
            }

            xmlTree(ce, layer, sb);
        }
    }

    /**
     * @param @param  json
     * @param @return
     * @return String
     * @throws
     * @Title: toModel
     * @Description: Change to JSON model
     */
    public static String toModel(String txt) {
        if (StringUtils.isEmpty(txt)) {
            return StringUtils.EMPTY;
        }

        if (isHtml(txt)) {
            return txt;
        }

        int layer = -1;
        StringBuilder sb = new StringBuilder();

        /* JSON */
        if (isJson(txt)) {
            JsonParser p = new JsonParser();
            JsonElement e = p.parse(txt);

            jsonTree(e, layer, sb);

            return sb.toString();
        }

        /* XML */
        if (isXml(txt)) {
            try {
                txt = StringUtils.replaceOnce(txt, "?>", "?><root>") + "</root>";
                Document doc = DocumentHelper.parseText(txt);
                xmlTree(doc.getRootElement(), layer, sb);
                return sb.toString();
            } catch (Exception e) {
                log.error("Bad xml format: " + lines(1) + txt);
            }
        }

        return sb.toString();
    }

    /**
     * @Title : loadHist
     * @Description: Load history
     * @Param : @param path if not specified, will use default.
     * @Param : @return
     * @Return : HttpHists
     * @Throws :
     */
    public static HttpHists loadHist(String path) {
        File fhist = null;
        if (StringUtils.isNotEmpty(path)) {
            fhist = new File(path);
            if (!fhist.exists()) {
                System.out.println("The historical file " + path + " does not exist, will use default " + RESTConst.HTTP_HIST_JSON);
                fhist = new File(RESTConst.HTTP_HIST_JSON);
            }
        } else {
            fhist = new File(RESTConst.HTTP_HIST_JSON);
        }

        if (!fhist.exists()) {
            System.out.println("The historical file " + path + " does not exist.");
            return null;
        }

        HttpHists hists = RESTUtil.toOject(fhist, HttpHists.class);
        if (null == hists || CollectionUtils.isEmpty(hists.getHists())) {
            System.out.println("No historical cases.");
        }
        return hists;
    }


    /**
     * @Title : closeSplashScreen
     * @Description: close splash screen
     * @Param :
     * @Return : void
     * @Throws :
     */
    public static void closeSplashScreen() {
        SplashScreen ss = SplashScreen.getSplashScreen();
        if (null == ss) {
            return;
        }
        try {
            ss.close();
        } catch (Exception e) {
            // Ignore this exception
        }
    }

    /**
     * @Title : excludeNode
     * @Description: Exclude JSON nodes
     * @Param : @param e
     * @Param : @param path
     * @Param : @param exclNodes, nodes to be excluded
     * @Return : void
     * @Throws :
     */
    private static void jsonTree(JsonElement e, String path, List<String> exclNodes) {
        if (e.isJsonNull()) {
            return;
        }

        if (e.isJsonPrimitive()) {
            return;
        }

        if (e.isJsonArray()) {
            JsonArray ja = e.getAsJsonArray();
            if (null != ja) {
                for (JsonElement ae : ja) {
                    jsonTree(ae, path, exclNodes);
                }
            }
            return;
        }

        if (e.isJsonObject()) {
            Map<String, JsonElement> tm = new LinkedHashMap<String, JsonElement>();
            for (Entry<String, JsonElement> en : e.getAsJsonObject().entrySet()) {
                tm.put(en.getKey(), en.getValue());
            }

            for (Entry<String, JsonElement> en : tm.entrySet()) {
                String nodeKey = path + "|" + en.getKey();
                if (CollectionUtils.isNotEmpty(exclNodes) && exclNodes.contains(nodeKey)) {
                    e.getAsJsonObject().remove(en.getKey());
                    continue;
                }
                jsonTree(en.getValue(), nodeKey, exclNodes);
            }
        }
    }

    /**
     * @Title : diff
     * @Description: Differ JSON nodes
     * @Param : @param json1
     * @Param : @param json2
     * @Param : @param excludedNodes
     * @Param : @return
     * @Return : boolean
     * @Throws :
     */
    public static boolean diff(String json1, String json2, List<String> exclNodes) {
        if (CollectionUtils.isEmpty(exclNodes)) {
            return json1.equals(json2);
        }

        JsonParser p = new JsonParser();
        JsonElement e1 = p.parse(json1);
        JsonElement e2 = p.parse(json2);

        jsonTree(e1, "", exclNodes);
        jsonTree(e2, "", exclNodes);

        return e1.equals(e2);
    }
}
